// This file is part of OpenTSDB.
// Copyright (C) 2012  The OpenTSDB Authors.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 2.1 of the License, or (at your
// option) any later version.  This program is distributed in the hope that it
// will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser
// General Public License for more details.  You should have received a copy
// of the GNU Lesser General Public License along with this program.  If not,
// see <http://www.gnu.org/licenses/>.
package net.opentsdb.tsd;

import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.handler.codec.frame.FrameDecoder;
import org.jboss.netty.handler.codec.frame.TooLongFrameException;

/**
 * Decodes telnet-style frames delimited by new-lines.
 * <p/>
 * Both "\n" and "\r\n" are handled.
 * <p/>
 * This decoder is stateful and is thus <strong>NOT</strong> shareable.
 */
final class LineBasedFrameDecoder extends FrameDecoder {

    /**
     * Maximum length of a frame we're willing to decode.
     */
    private final int max_length;
    /**
     * True if we're discarding input because we're already over max_length.
     */
    private boolean discarding;

    /**
     * Creates a new decoder.
     *
     * @param max_length Maximum length of a frame we're willing to decode.
     *                   If a frame is longer than that, a {@link org.jboss.netty.handler.codec.frame.TooLongFrameException} will
     *                   be fired on the channel causing it.
     */
    public LineBasedFrameDecoder(final int max_length) {
        this.max_length = max_length;
    }

    @Override
    protected Object decode(final ChannelHandlerContext ctx,
                            final Channel channel,
                            final ChannelBuffer buffer) throws Exception {
        final int eol = findEndOfLine(buffer);
        if (eol != -1) {
            final ChannelBuffer frame;
            final int length = eol - buffer.readerIndex();
            assert length >= 0 : "WTF?  length=" + length;
            if (discarding) {
                frame = null;
                buffer.skipBytes(length);
            } else {
                frame = buffer.readBytes(length);
            }
            final byte delim = buffer.readByte();
            if (delim == '\r') {
                buffer.skipBytes(1);  // Skip the \n.
            }
            return frame;
        }

        final int buffered = buffer.readableBytes();
        if (!discarding && buffered > max_length) {
            discarding = true;
            Channels.fireExceptionCaught(ctx.getChannel(),
                    new TooLongFrameException("Frame length exceeds " + max_length + " ("
                            + buffered + " bytes buffered already)")
            );
        }
        if (discarding) {
            buffer.skipBytes(buffer.readableBytes());
        }
        return null;
    }

    /**
     * Returns the index in the buffer of the end of line found.
     * Returns -1 if no end of line was found in the buffer.
     */
    private static int findEndOfLine(final ChannelBuffer buffer) {
        final int n = buffer.writerIndex();
        for (int i = buffer.readerIndex(); i < n; i++) {
            final byte b = buffer.getByte(i);
            if (b == '\n') {
                return i;
            } else if (b == '\r' && i < n - 1 && buffer.getByte(i + 1) == '\n') {
                return i;  // \r\n
            }
        }
        return -1;  // Not found.
    }

}
